package com.nulldev.util.networking.npacket4.micropacket;

import static com.nulldev.util.networking.npacket4.util.nPacketUtil.__heatshrink;
import static com.nulldev.util.networking.npacket4.util.nPacketUtil.__read;
import static com.nulldev.util.networking.npacket4.util.nPacketUtil.__readASCII;
import static com.nulldev.util.networking.npacket4.util.nPacketUtil.__readInt;
import static com.nulldev.util.networking.npacket4.util.nPacketUtil.__unheatshrink;
import static com.nulldev.util.networking.npacket4.util.nPacketUtil.__write;
import static com.nulldev.util.networking.npacket4.util.nPacketUtil.__writeASCII;
import static com.nulldev.util.networking.npacket4.util.nPacketUtil.__writeInt;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Arrays;

import com.nulldev.util.JVM.JVM;
import com.nulldev.util.data.Variables;
import com.nulldev.util.data.CompressionAPI.thirdparty.airlift.snappy.SnappyCompressor;
import com.nulldev.util.data.CompressionAPI.thirdparty.airlift.snappy.SnappyDecompressor;
import com.nulldev.util.data.CompressionAPI.thirdparty.heatshrink.HeatShrinkDecoder;
import com.nulldev.util.data.CompressionAPI.thirdparty.heatshrink.HeatShrinkEncoder;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.manage.MarkedForRemoval;
import com.nulldev.util.networking.npacket4.extensions.PacketExtension;
import com.nulldev.util.networking.npacket4.extensions.PacketExtensions;
import com.nulldev.util.networking.npacket4.extensions.protocol.PacketCompressionExtension;
import com.nulldev.util.networking.npacket4.extensions.security.PacketSecurer;
import com.nulldev.util.networking.npacket4.extensions.security.SecureCommunicationExtension;

public class nMicroPacket {
	public static final int MICROPACKET_HEADER = 77425; // NMP
	public static final int MAXIMUM_PACKET_SIZE = 4096;
	public static final int MAX_COMP_DATA_SIZE = 4088;
	public static final int _COMP_WINDOW_SIZE = 10;
	public static final int _COMP_LOOK_AHEAD_COUNT = 4;

	/* */

	public static final int MICROPACKET_COMPRESSION_FORMAT_HS = 1;
	public static final int MICROPACKET_COMPRESSION_FORMAT_SNAPPY = 2;
	/* */
	@Deprecated
	@MarkedForRemoval(reason = "Removed in build 360.")
	public static final int MICROPACKET_COMPRESSION_FORMAT_KANZI_PROPOSAL_1 = 65534;

	/* */

	/**
	 * Packet Layout
	 * 
	 * Features: <br>
	 * - Compressed using various algorithms <br>
	 * - Limited to 4kB [32 kb] <br>
	 * - Higher priority than nPacket(s)
	 * 
	 * TODO:<br>
	 * - Add support for MORE in heatshrink.<br>
	 * - Add partial-support for MUX. (although a 4kB packet really shouldn't be
	 * MUX'd)
	 * 
	 * |-----------------------------------------------------------| <br>
	 * |HEADER (4) | PKT_LENGTH(4) | COMPRESSED_PACKET (up to 4088)|
	 * |-----------------------------------------------------------|
	 */

	private volatile PacketExtension[] packetExts = new PacketExtension[0];
	private volatile int[] packetExtIDs = new int[0];
	private volatile String packetID;
	private volatile byte[] data;
	private volatile byte[] raw_data;
	private volatile PacketSecurer sec;
	public volatile boolean USE_LEGACY_FROM_DATA_WRITE;

	public nMicroPacket() {
	}

	public nMicroPacket(final nMicroPacket packet) {
		if (packet == null)
			throw new NullPointerException("packet is null!");
		this.packetExtIDs = packet.packetExtIDs;
		this.packetExts = packet.packetExts;
		this.packetID = packet.packetID;
		this.data = packet.data;
		this.raw_data = packet.raw_data;
		this.sec = packet.sec;
		this.USE_LEGACY_FROM_DATA_WRITE = packet.USE_LEGACY_FROM_DATA_WRITE;
		try {
			final DataInputStream dis = new DataInputStream(new ByteArrayInputStream(this.data));
			this.onPacket(dis);
			IOUtils.closeQuietly(dis);
		} catch (IOException e) {
			JVM.errorln(e);
		}
	}

	public nMicroPacket(final String packetID, final nMicroPacket packet) {
		if (packetID == null || packetID.isEmpty())
			throw new NullPointerException("Invalid packet ID!");
		if (packet == null)
			throw new NullPointerException("packet is null!");
		this.packetExtIDs = packet.packetExtIDs;
		this.packetExts = packet.packetExts;
		this.packetID = packetID;
		this.data = packet.data;
		this.raw_data = packet.raw_data;
		this.sec = packet.sec;
		this.USE_LEGACY_FROM_DATA_WRITE = packet.USE_LEGACY_FROM_DATA_WRITE;
		try {
			final DataInputStream dis = new DataInputStream(new ByteArrayInputStream(this.data));
			this.onPacket(dis);
			IOUtils.closeQuietly(dis);
		} catch (IOException e) {
			JVM.errorln(e);
		}
	}

	public nMicroPacket(final String packetID, final PacketExtension... exts) {
		if (packetID == null || packetID.isEmpty())
			throw new NullPointerException("Invalid packet ID!");
		this.packetID = packetID;
		this.packetExts = exts;
		this.packetExtIDs = new int[exts.length];
		for (int i = 0; i < exts.length; i++) {
			this.packetExtIDs[i] = exts[i].extensionID();
		}
	}

	public nMicroPacket(final String packetID, final PacketSecurer sec, final PacketExtension... exts) {
		if (packetID == null || packetID.isEmpty())
			throw new NullPointerException("Invalid packet ID!");
		if (sec == null)
			throw new NullPointerException("sec is null!");
		this.packetID = packetID;
		this.packetExts = exts;
		this.packetExtIDs = new int[exts.length];
		for (int i = 0; i < exts.length; i++) {
			this.packetExtIDs[i] = exts[i].extensionID();
		}
	}

	public nMicroPacket(final PacketSecurer sec) throws IOException {
		if (sec == null)
			throw new NullPointerException("sec is null!");
		this.sec = sec;
	}

	public String getPacketID() {
		return this.packetID;
	}

	public nMicroPacket readPacket(final byte[] data) throws IOException {
		Variables.notNullE(data);
		if (data.length == 0)
			throw new IllegalArgumentException("data.length == 0!");
		this.raw_data = data;
		final ByteArrayInputStream cbais = new ByteArrayInputStream(data);
		final int header = __readInt(cbais);
		if (header != MICROPACKET_HEADER) {
			throw new UnsupportedOperationException("nMicroPacket: Invalid packet!");
		}
		final int compressionFormat = __readInt(cbais);
		final int packetLength = __readInt(cbais);
		final byte[] cmpPacket = new byte[packetLength];
		__read(cbais, cmpPacket);
		final byte[] decPacket;
		switch (compressionFormat) {
			case MICROPACKET_COMPRESSION_FORMAT_HS: {
				final HeatShrinkDecoder hse = new HeatShrinkDecoder(_COMP_WINDOW_SIZE, _COMP_LOOK_AHEAD_COUNT, packetLength);
				decPacket = __unheatshrink(hse, cmpPacket);
				break;
			}
			case MICROPACKET_COMPRESSION_FORMAT_SNAPPY: {
				final SnappyDecompressor sd = new SnappyDecompressor();
				decPacket = new byte[SnappyDecompressor.getUncompressedLength(cmpPacket, 0)];
				sd.decompress(cmpPacket, decPacket);
				break;
			}
			default:
				throw new UnsupportedOperationException("Unknown compression format:" + compressionFormat);
		}
		final ByteArrayInputStream bais = new ByteArrayInputStream(decPacket);
		this.packetID = __readASCII(bais);
		final int extensions = __readInt(bais);
		this.packetExts = new PacketExtension[extensions];
		this.packetExtIDs = new int[extensions];
		if (extensions > 0) {
			for (int i = 0; i < extensions; i++) {
				this.packetExtIDs[i] = __readInt(bais);
			}
		}
		final int size = __readInt(bais);
		byte[] _data = new byte[size];
		__read(bais, _data);
		this.data = _data;
		final int extensionCount = __readInt(bais);
		if (extensionCount > 0) {
			for (int i = 0; i < extensionCount; i++) {
				final int extID = __readInt(bais);
				if (extID == PacketExtensions.PacketExtensionID.EXT_ENCRYPTED && sec != null) {
					this.packetExts[i] = new SecureCommunicationExtension(sec).readExtensionData(bais);
					this.data = this.packetExts[i].processData(this.data);
				}
			}
		}
		bais.close();
		try (final DataInputStream _dis = new DataInputStream(new ByteArrayInputStream(this.data))) {
			this.onPacket(_dis);
		} catch (Exception e) {
			JVM.errorln(e);
		}
		return this;
	}

	public void onPacket(final DataInputStream dis) throws IOException {
	}

	public void onGenerate(final DataOutputStream dos) throws IOException {
	}

	public byte[] makePacket() {
		return makePacket(MICROPACKET_COMPRESSION_FORMAT_HS);
	}

	public byte[] makePacket(final int compressionFormat) {
		try {
			final ByteArrayOutputStream baos = new ByteArrayOutputStream(IOUtils.MEMORY_ALLOC_BUFFER);
			__writeInt(baos, MICROPACKET_HEADER);
			final ByteArrayOutputStream mainDataStream = new ByteArrayOutputStream(IOUtils.MEMORY_ALLOC_BUFFER);
			__writeASCII(mainDataStream, this.packetID);
			__writeInt(mainDataStream, this.packetExts.length);
			for (final int ext : this.packetExtIDs) {
				__writeInt(mainDataStream, ext);
			}
			mainDataStream.flush();
			if (!this.USE_LEGACY_FROM_DATA_WRITE) {
				final ByteArrayOutputStream dbaos = new ByteArrayOutputStream(IOUtils.MEMORY_ALLOC_BUFFER);
				final DataOutputStream ddos = new DataOutputStream(dbaos);
				this.onGenerate(ddos);
				ddos.flush();
				this.data = dbaos.toByteArray();
				ddos.close();
			}
			for (final PacketExtension ext : this.packetExts) {
				if (ext instanceof SecureCommunicationExtension) {
					this.data = sec.encrypt(this.data);
				} else if (ext instanceof PacketCompressionExtension) {
					// UNSUPPORTED
				}
			}
			__writeInt(baos, compressionFormat);
			__writeInt(mainDataStream, this.data.length);
			mainDataStream.flush();
			__write(mainDataStream, this.data);
			mainDataStream.flush();
			__writeInt(mainDataStream, this.packetExts.length);
			mainDataStream.flush();
			for (int i = 0; i < this.packetExts.length; i++) {
				final PacketExtension e = this.packetExts[i];
				final byte[] ed = e.extensionData();
				__writeInt(mainDataStream, e.extensionID());
				mainDataStream.flush();
				__write(mainDataStream, ed);
				mainDataStream.flush();
			}
			final byte[] output;
			switch (compressionFormat) {
				case MICROPACKET_COMPRESSION_FORMAT_HS: {
					final HeatShrinkEncoder enc = new HeatShrinkEncoder(_COMP_WINDOW_SIZE, _COMP_LOOK_AHEAD_COUNT);
					output = __heatshrink(enc, mainDataStream.toByteArray());
					break;
				}
				case MICROPACKET_COMPRESSION_FORMAT_SNAPPY: {
					final SnappyCompressor sc = new SnappyCompressor();
					output = new byte[sc.maxCompressedLength(mainDataStream.size())];
					sc.compress(mainDataStream.toByteArray(), output);
					break;
				}
				default:
					throw new UnsupportedOperationException("Unknown compression format:" + compressionFormat);
			}
			if (output.length > MAX_COMP_DATA_SIZE)
				throw new UnsupportedOperationException("Packet size too large! [got: " + output.length + ", max: " + MAX_COMP_DATA_SIZE + "]");
			__writeInt(baos, output.length);
			__write(baos, output);
			return baos.toByteArray();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public String toString() {
		return "nMicroPacket[version=1,id=" + this.packetID + ",size=" + this.data.length + ",exts="
				+ Arrays.toString(PacketExtensions.getExtensionsReadable(this.packetExtIDs)) + "]";
	}

	public int[] getExtensionIDs() {
		return this.packetExtIDs;
	}

	public PacketExtension[] getExtensions() {
		return this.packetExts;
	}

	public static void main(String[] args) throws Exception {
		final nMicroPacket micro = new nMicroPacket("test.pack") {
			@Override
			public void onGenerate(DataOutputStream dos) throws IOException {
				dos.writeUTF("\n" + "\n"
						+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum gravida congue risus, gravida pretium sapien hendrerit nec. Nullam mattis ipsum vitae purus consequat rhoncus. Fusce nisi arcu, scelerisque ac eros non, cursus rhoncus ipsum. Morbi pharetra commodo nisl non bibendum. Quisque ac magna odio. Aliquam vel mattis lacus. Nam ornare, tortor ut hendrerit placerat, turpis metus tempus est, id rhoncus massa velit a nibh. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae;\n"
						+ "\n"
						+ "Nulla egestas, odio eget tincidunt maximus, ante elit condimentum tellus, nec eleifend nunc massa ac nisl. Ut iaculis sodales vehicula. Nam ipsum sem, finibus eget bibendum at, dictum vel lorem. Integer blandit leo mauris, et vehicula leo rutrum a. Nam congue nisi lacus, nec commodo justo ornare sed. Pellentesque aliquet sem ut finibus interdum. Donec porttitor rhoncus lectus, vel iaculis enim posuere ut. Sed quis pretium ante, quis semper sapien. Fusce in est tellus. Quisque at libero ut sem efficitur rutrum. In hac habitasse platea dictumst. Praesent mollis aliquam erat a tincidunt.\n"
						+ "\n"
						+ "Mauris neque libero, lacinia at sodales nec, maximus a ligula. Sed sodales tellus ac eros posuere, at malesuada nulla venenatis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque pellentesque leo a sapien fringilla iaculis. Nunc molestie, augue posuere feugiat accumsan, enim libero semper sem, a finibus mi purus nec orci. Phasellus finibus consequat odio, id condimentum magna pulvinar sed. Sed a aliquam nunc. Etiam convallis sem in ante sagittis auctor. Nullam fringilla ante nisi, at volutpat arcu accumsan sed. Interdum et malesuada fames ac ante ipsum primis in faucibus.\n"
						+ "\n"
						+ "Nullam orci dolor, sollicitudin sit amet placerat at, iaculis non augue. Nam at laoreet velit. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque vel metus eu ipsum placerat sagittis. Proin massa dui, lobortis ac nisl ut, lobortis gravida tellus. In non facilisis nibh. Vivamus sit amet luctus lacus, vel molestie urna. Etiam fringilla, enim nec lacinia cursus, velit lectus faucibus massa, sit amet egestas tortor lacus auctor massa. Morbi vitae venenatis nibh, at condimentum velit.\n"
						+ "\n"
						+ "Cras vulputate, sapien id varius faucibus, justo purus aliquam dui, sed hendrerit leo purus eget risus. Ut porttitor tristique tellus, ac blandit tellus. Nullam accumsan ornare aliquam. Maecenas tincidunt sem nec viverra facilisis. Nullam egestas lacus sit amet velit condimentum rhoncus. Sed ac ornare tortor. Phasellus tincidunt eu lorem vitae varius. Proin euismod rutrum facilisis. Aliquam eu feugiat neque. Fusce varius felis non magna feugiat vehicula sed quis arcu. Ut ac enim quam. Donec porta luctus mauris eget interdum. Nullam sit amet pellentesque enim, pharetra varius eros. Aliquam fringilla volutpat libero, vitae ornare purus condimentum vel. Integer a odio risus. ");
			}
		};
		final byte[] b = micro.makePacket(MICROPACKET_COMPRESSION_FORMAT_HS);
		JVM.println("[enc] size: " + b.length);
		JVM.println(new String(b));
		final nMicroPacket dec = new nMicroPacket().readPacket(b);
		JVM.println("[dec] size: " + dec.dataLength());
		final DataInputStream dis = new DataInputStream(new ByteArrayInputStream(dec.data));
		JVM.println(dis.readUTF());
		IOUtils.closeQuietly(dis);
	}

	public int dataLength() {
		return this.data.length;
	}

}
